fix: preserve selected page state across the maintenance mode lifecycle - #524
fix: preserve selected page state across the maintenance mode lifecycle#524Alexia-Soare wants to merge 11 commits into
Conversation
…de lifecycle Selecting an existing published page as the maintenance page no longer leaves it private (404 for visitors) after maintenance mode is disabled or the plugin is deactivated. The page's original status and template are now recorded in post meta when the page is selected (or on first enable, for pages selected before this fix) and restored when maintenance mode is disabled and on plugin deactivation. Pages with no recorded state are left untouched instead of being forced private. Fixes #523 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…plate Applying a gallery template while an existing site page was selected overwrote that page's content and forced it private. Templates are now written only to pages the plugin created (marked with _wpmm_generated); when a user-owned page is selected, it is handed back in its original state and the template lands on a newly created maintenance page. Recording a page's original state is now first-write-wins and the record is cleared on restore, so re-selecting a page the plugin already modified can no longer poison the saved original state. Also guards the Otter CSS_Handler call, which fataled on sites without Otter Blocks. Refs #523 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skip the enable-mode page takeover when the selected page no longer exists (previously wrote orphan post meta), and reject template imports whose slug/category do not resolve to a bundled template instead of creating an empty maintenance page. Template slug and category are sanitized before being used in a filesystem path. Refs #523 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restoring the selected page's recorded state now leaves trashed pages alone (trashing is always user intent; the plugin never trashes), and enabling maintenance mode with a trashed page selected records nothing, so untrashing the page later cannot snap it back to trash. Refs #523 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Selecting an existing published page via the real AJAX handler, enabling and disabling maintenance mode must leave the page public with its original content and template. Verified to fail against the unfixed code. Refs #523 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The job installed the latest WordPress test suite (currently 7.0.1, which requires PHP 7.4+) on PHP 7.2, so the suite aborted before running any test. The phpunit:7.5.20 tool pin was unused: composer run-script resolves vendor/bin/phpunit from the lockfile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Preserves selected maintenance-page state and prevents user-owned pages from being overwritten.
Changes:
- Records and restores page status/template across maintenance lifecycle events.
- Restricts template overwrites to plugin-generated pages and validates imports.
- Adds lifecycle tests and updates CI to PHP 7.4.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
views/settings.php |
Limits overwrite warnings to generated pages. |
tests/page-state-test.php |
Adds lifecycle and template-import tests. |
includes/functions/helpers.php |
Adds page-state record/restore helpers. |
includes/classes/wp-maintenance-mode.php |
Integrates restoration with enable, disable, and deactivation. |
includes/classes/wp-maintenance-mode-admin.php |
Adds ownership-aware selection and template importing. |
.github/workflows/test-php.yml |
Updates PHPUnit CI to PHP 7.4. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…mically Addresses the Copilot review on #524: - select_page() restores the previously selected page before switching away from it, so the old page never keeps the maintenance template - restoring a trashed page keeps it in the trash but hands back the template and clears the record, so untrashing brings it back clean - recording claims the record with add_post_meta( ..., true ) so a racing request cannot capture the plugin-modified state as original Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
includes/classes/wp-maintenance-mode-admin.php:696
- The previous page is restored while
wpmm_settingsstill selects it until line 713. If maintenance mode is active, a frontend request in that interval boots from the old option and takes that page over again; this callback then commits the new ID, leaving the abandoned page with the maintenance template and stale state metadata. Commit/gate the selection transition before restoring the old page, or serialize it with frontend takeover.
if ( $previous_page_id && $previous_page_id !== $page_id ) {
// hand the previously selected page back before abandoning it,
// otherwise nothing would ever restore it
wpmm_restore_page_state( $previous_page_id );
includes/functions/helpers.php:542
add_post_meta(..., true)is not an atomic first-write-wins claim: WordPress checks for existing metadata before inserting, andwp_postmetahas no uniqueness constraint, so racing requests can both insert records. The template is also written separately afterward, allowing restore to consume a partial status-only record. Use storage with an actual unique constraint/compare-and-set or a lock, and persist status plus template atomically.
// the unique flag makes the status key a first-write-wins claim on the
// record, so a racing request cannot save the plugin-modified state
if ( ! add_post_meta( $page_id, '_wpmm_original_status', $status, true ) ) {
return;
}
update_post_meta( $page_id, '_wpmm_original_template', $template );
The legacy eslint toolchain caps its node engines below current node versions while the new test tooling requires node 18+, so every yarn install now runs with --ignore-engines and the zip/deploy workflows move from node 14 to 20. The PHPUnit job moves to PHP 7.4, required by the latest WordPress test suite (same fix as #524; trivial conflict). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
includes/functions/helpers.php:542
- The unique status write does not make the two-key state record atomic. A concurrent disable/switch can restore after
_wpmm_original_statusis added but before_wpmm_original_templateis written, deleting the real template and clearing the claim; conversely, restore's separate deletes can remove a new takeover's record. Store status and template together in one first-write-wins meta value and consume that record atomically (or add equivalent synchronization).
// the unique flag makes the status key a first-write-wins claim on the
// record, so a racing request cannot save the plugin-modified state
if ( ! add_post_meta( $page_id, '_wpmm_original_status', $status, true ) ) {
return;
}
update_post_meta( $page_id, '_wpmm_original_template', $template );
…ve-selected-page-state # Conflicts: # .github/workflows/test-php.yml
wp_insert_post() without the wp_error flag returns an int, so the instanceof WP_Error guard was dead code; the page-state helpers also document their void return now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the follow-up Copilot review on #524: - the plugin marks the publish it applies to a private page, and restore only reverts the status while the page still carries that plugin-made publish; a draft/private/trash the user chose in the meantime wins - the original state lives in a single first-write-wins meta value, so a racing request can neither poison the record nor observe half of it - a maintenance template carried over from an old release is recorded as no template, so restore removes it instead of making it permanent Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
includes/classes/wp-maintenance-mode.php:110
- A user-selected
privatestatus is not preserved here. After a page has been taken over, any later request while maintenance mode is active seesprivateand publishes it again; this also happens when the originally published page was made private by the user, because there is no applied-status marker yet. Gate publishing on the recorded original status being private and on publish not having already been applied, so later user changes are not undone.
if ( get_post_status( $this->plugin_settings['design']['page_id'] ) === 'private' ) {
includes/classes/wp-maintenance-mode-admin.php:773
- This condition excludes trashed pages because
$page_existsis false fortrash. If a selected user page is trashed and a gallery template is imported, the selection moves to the new page without restoring the old page, leaving its maintenance template and state metadata behind; restoring it from trash later exposes that stale plugin state. Restore any still-existing selected post here, including trash.
if ( $page_exists ) {
// hand the user's page back before switching to a generated one
wpmm_restore_page_state( $selected_page_id );
Selecting an existing published page (e.g. the homepage) as the maintenance page could permanently break it (#523): the plugin forced it
privateon every load with maintenance mode disabled, and applying a gallery template overwrote its content. The plugin now records a selected page's original state and restores it, and only writes templates into pages it created itself.What changed
select_page()(or first enable, for pre-existing selections) records the page's original status and template in a single post meta value, claimed first-write-wins, so a racing request can neither capture plugin-made changes as "original" nor observe half a record. The record is cleared after each restore, so later user edits become the new baseline; a maintenance template carried over from an old release is recorded as "no template" rather than as user state.private; disabling and plugin deactivation now restore the recorded status and template, and leave pages with no record untouched._wpmm_generated); a selected user-owned page is restored and the template lands on a newly created "Maintenance Page", which becomes the selection. The "content will be replaced" warning in settings now shows only in that overwrite case.CSS_Handlercall is skipped when Otter Blocks isn't installed (previously a fatal error).The CI job also moves from PHP 7.2 to 7.4: the latest WordPress test suite requires 7.4, so PHPUnit aborted before running any test. It's bundled here because this PR's tests depend on it.
Lifecycle after this PR
flowchart TD A[User selects a page] --> P[Restore previously<br/>selected page, if any]:::added P --> B[Record original status + template]:::added B --> C[Enable: apply wpmm template,<br/>publish page, mark publish as plugin-made]:::changed C --> D{Disable / deactivate} D --> E[Undo the plugin-made publish only,<br/>restore template, clear record;<br/>user status changes stay]:::added F[Template import] --> G{Page marked<br/>_wpmm_generated?}:::added G -- yes --> H[Overwrite it with the template] G -- no --> I[Restore user page; create new<br/>generated page and select it]:::added classDef added fill:#1a7f37,color:#fff,stroke:#1a7f37 classDef changed fill:#9a6700,color:#fff,stroke:#9a6700🟩 added by this PR (restore on switch, record/restore, ownership check, new-page branch) · 🟨 changed behavior (enable now records state, marks its publish, and skips deleted/trashed pages) · gray = unchanged
Data changes
Before: no stored record — disable ran
post_status → privateunconditionally. After: state is recorded on takeover and consumed on restore._wpmm_original_state_wpmm_applied_statuspublish, when it publishes a private page)_wpmm_generated1on pages the plugin createdQA
privateand visitors got a 404.